Skip to content

Serial (CDC) module for USB Host #10283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

rianadon
Copy link

This is a module wrapping TinyUSB's class/cdc/cdc_host library into a pyserial-like API so that a CircuitPython board with a USB Host can talk to other devices with CDC interfaces over USB.

It is possible to talk CDC using only the usb.core.Device.read/.write methods. However, this only works in blocking mode. I tried setting up asynchronous communication by supplying a timeout to .read(), but when I tested my code I missed data.
As I understand, CDC reads are performed by sending an IN packet, then waiting for the device to eventually send a DATA packet back. This transaction cannot be cancelled halfway through, so even if CircuitPython times out and moves onto other things, the data transfer is going to happen regardless.

I think the only way to build an asynchronous API around sending data through the usb.core library is to use FIFOs so that even if CircuitPython times out and moves on, the data is still appended to the FIFO and can be read back later. This is how TinyUSB's Endpoint Stream API works. The class/cdc/cdc_host library I am wrapping internally uses Endpoint Streams for the rx and tx endpoints.

I was debating building a module around the cdc host or endpoint streams and decided against endpoint streams since they are only exposed in TinyUSB's private headers. They don't require a significant amount of code to implement though, so if you think that it is better to build a lower-level library than a higher-level library I could submit that as a PR instead. I have most of the code written, but it does not work yet.

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the pyserial approach! I'd go 100% into it and match APIs exactly and then call it serial. That way code will just work in CPython too.

You'll want to conditionalize enabling it so that builds without USB Host don't enable it. That should fix the CI.

@@ -2968,6 +2968,10 @@ msgstr ""
msgid "destination buffer must be an array of type 'H' for bit_depth = 16"
msgstr ""

#: shared-bindings/usb/cdc_host/__init__.c
msgid "device must be a usb.core.Device object"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a new message, reuse "%q must be of type %q, not %q". (I'd search for where it is used to copy it's example.)

Comment on lines +788 to +789
usb/cdc_host/Serial.c \
usb/cdc_host/__init__.c \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely needs to match the other whitespace.

static const mp_rom_map_elem_t usb_cdc_host_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_usb_dot_cdc_host) },
{ MP_ROM_QSTR(MP_QSTR_Serial), MP_ROM_PTR(&usb_cdc_host_serial_type) },
{ MP_ROM_QSTR(MP_QSTR_find), MP_ROM_PTR(&usb_cdc_host_find_obj) },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't add a find. Instead, match pyserial exactly with: https://pyserial.readthedocs.io/en/latest/tools.html#serial.tools.list_ports.comports

@rianadon
Copy link
Author

Thanks for the feedback!

I was thinking some more about the tradeoffs between implementing the low level endpoint API vs high level serial API, and I changed my mind and am leaning more towards the EndpointStream for a few reasons:

  • An API that dynamically allocates a fifo for reading from an endpoint would support infinite endpoints. However, with TinyUSB's CDC class, the number of USB communication ports supported on the host depends on CFG_TUH_CDC. You can increase it to deal with devices that spawn many CDC ports, but this comes at the expense of memory.
  • EndpointStream solves non-blocking USB for any device, not just serial. This might be useful for keyboards, midi, etc. The pyserial-compatible API could be provided by a Python module wrapping the EndpointStream.
  • It requires fewer functions to implement.

Curious to hear your thoughts. I can close this and open a new PR.

@tannewt
Copy link
Member

tannewt commented Apr 28, 2025

It's up to you what you'd like to implement.

I'd lean against EndpointStream because it'd be a new unique API. This higher level has the advantage of matching the existing pyserial api. The low level core APIs match pyusb in CPython.

Is there a pyusb version of EndpointStream?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants